import time

from coin.base.timestamp import get_timestamp
import coin.proto.coin_order_enums_pb2 as coin_order
from coin.proto.coin_query_pb2 import (ProductOrderElement, ProductPosition)
import coin.proto.account_control_pb2 as control_proto
import coin.proto.coin_request_pb2 as acc_request_proto
from coin.support.account_control.rest_util import RestAccountManager
from coin.support.account_control.util import (control_target_to_account_request,
                                               generate_product_from_control_target)


def _get_order_limit_info(product):
  try:
    if product.exchange == 'OkexFutures':
      tick_size = 0.01 if product.base in ('BTC', 'BTG') else 0.001
      max_order_size = 200 if product.base in ('BTC', 'BTG') else 2000
    elif product.exchange == 'Bitmex':
      assert product.base == 'BTC' and product.quote == 'USD'
      tick_size = 0.5
      max_order_size = 20000
    elif product.exchange == 'Bitflyer':
      assert product.base == 'BTC' and product.quote == 'JPY'
      tick_size = 1
      max_order_size = 2
    elif product.exchange == 'Bitfinex':
      assert product.base == 'BTC' and product.quote == 'USD'
      tick_size = 1
      max_order_size = 2
    else:
      raise
  except Exception:
    raise ValueError('Fail to get tick size. exchange: %s, symbol: %s' %
                     (product.exchange, product.symbol))
  return tick_size, max_order_size


def _gen_control_response(target: acc_request_proto.AccountRequestProto,
                          account_operation: control_proto.AccountOperationResponse):
  control_response = control_proto.ControlResponse(target=target,
                                                   type=control_proto.CONTROL_ACCOUNT_OPERATION,
                                                   account_operation=account_operation)
  return control_response


class RestAccountOperationExecutor(object):
  def __init__(self, control_target: acc_request_proto.AccountRequestProto):
    self._control_target = control_target
    account_request = control_target_to_account_request(control_target)
    self._account_manager = RestAccountManager(account_request)
    self._public_client = self._account_manager.public_client
    self._private_client = self._account_manager.private_client

  @classmethod
  def request(cls, control_request: control_proto.ControlRequest):
    target = control_request.target
    operation_executor = cls(target)
    operation_type = control_request.account_operation.operation_type
    if control_request.target.HasField('product'):
      product = generate_product_from_control_target(target)
    if operation_type == control_proto.QUERY_ACCOUNT_BALANCE:
      control_response = operation_executor.query_account_balance()
    elif operation_type == control_proto.QUERY_ACCOUNT_POSITION:
      control_response = operation_executor.query_account_position()
    elif operation_type == control_proto.QUERY_ORDERS:
      control_response = operation_executor.query_working_order(product)
    elif operation_type == control_proto.CANCEL_ALL:
      control_response = operation_executor.cancel_all(product)
    elif operation_type == control_proto.NET_ZERO:
      control_response = operation_executor.net_zero(product)
    else:
      raise ValueError('Operation type not supported. %s' % operation_type)
    return control_response

  def query_account_balance(self):
    update = self._private_client.query_account_balance()
    operation_response = control_proto.AccountOperationResponse(
        success=True,
        operation_type=control_proto.QUERY_ACCOUNT_BALANCE,
        account_balance=update.msg)
    return _gen_control_response(self._control_target, operation_response)

  def query_account_position(self):
    update = self._private_client.query_account_position()
    operation_response = control_proto.AccountOperationResponse(
        success=True,
        operation_type=control_proto.QUERY_ACCOUNT_POSITION,
        account_position=update.msg)
    return _gen_control_response(self._control_target, operation_response)

  def query_working_order(self, product):
    update = self._private_client.query_list_orders(product)
    target = self._gen_product_target(self._control_target, product)
    operation_response = control_proto.AccountOperationResponse(
        success=True, operation_type=control_proto.QUERY_ORDERS, product_order=update.msg)
    return _gen_control_response(target, operation_response)

  def cancel_all(self, product):
    self._account_manager.ensure_working_orders_are_canceled(product, max_num_retries=2)
    target = self._gen_product_target(self._control_target, product)
    operation_response = control_proto.AccountOperationResponse(
        success=True, operation_type=control_proto.CANCEL_ALL)
    return _gen_control_response(target, operation_response)

  def net_zero(self, product):
    tick_size, max_order_size = _get_order_limit_info(product)
    while True:
      self._account_manager.ensure_working_orders_are_canceled(product)
      position = self._query_position(product)
      net_position = position.net_position
      if net_position == 0:
        break
      price = self._get_order_price(product, net_position, tick_size)
      side = self._get_order_side(product, net_position)
      left_position = abs(net_position)
      while left_position > max_order_size:
        order_element = ProductOrderElement(symbol=product.symbol,
                                            price=price,
                                            qty=max_order_size,
                                            side=side,
                                            created_timestamp=get_timestamp())
        self._account_manager.submit_order(product, order_element)
        left_position -= max_order_size
        time.sleep(0.5)
      order_element = ProductOrderElement(symbol=product.symbol,
                                          price=price,
                                          qty=left_position,
                                          side=side,
                                          created_timestamp=get_timestamp())
      self._account_manager.submit_order(product, order_element)
      time.sleep(0.5)

    target = self._gen_product_target(self._control_target, product)
    operation_response = control_proto.AccountOperationResponse(
        success=True, operation_type=control_proto.NET_ZERO)
    return _gen_control_response(target, operation_response)

  def _gen_product_target(self, control_target, product):
    if control_target.HasField('product'):
      assert control_target.product == product.symbol, 'Inconsistent product.'
    target = acc_request_proto.AccountRequestProto()
    target.CopyFrom(control_target)
    target.product = product.symbol
    return target

  def _query_position(self, product):
    update = self._private_client.query_account_position()
    product_position = None
    for position in update.msg.each_position:
      if position.symbol == product.symbol:
        assert product_position is None, \
          'Duplicate product position.  exchange: %s, symbol: %s' % (
              product.exchange, product.symbol)
        product_position = position
    product_position = product_position or ProductPosition(
        symbol=product.symbol, native_symbol=product.native_symbol, net_position=0)
    assert product_position.HasField('symbol') and \
           product_position.HasField('net_position')
    return product_position

  def _get_order_price(self, product, net_position, tick_size):
    ticker = self._public_client.query_ticker(product).msg
    assert ticker.HasField('last')
    if net_position > 0:
      price = ticker.bid if ticker.HasField('bid') else ticker.last
      price = price - 6 * tick_size
    elif net_position < 0:
      price = ticker.ask if ticker.HasField('ask') else ticker.last
      price = price + 6 * tick_size
    else:
      price = ticker.last
    return price

  def _get_order_side(self, product, net_position):
    if net_position < 0:
      side = coin_order.BUY_CLOSE_ORDER if product.exchange == 'OkexFutures' \
             else coin_order.BUY_ORDER
    elif net_position > 0:
      side = coin_order.SELL_CLOSE_ORDER if product.exchange == 'OkexFutures' \
             else coin_order.SELL_ORDER
    else:
      side = coin_order.UNKNOWN_ORDER_SIDE
    return side
